package org.atlantis.datasink;

import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.ServiceLoader;

import org.atlantis.datasink.cfg.OptionIniter;
import org.atlantis.datasink.codec.CodecOption;
import org.atlantis.datasink.codec.DeviceIdMapping;
import org.atlantis.datasink.codec.ICodec;
import org.atlantis.datasink.codec.ICodecContext;
import org.atlantis.datasink.event.EventManager;
import org.atlantis.datasink.server.IServer;
import org.atlantis.datasink.server.buildin.ActiveTCPServer;
import org.atlantis.datasink.server.buildin.PassiveTCPServer;
import org.atlantis.datasink.utils.SubstepAwait;
import org.atlantis.datasink.utils.Utils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

public class StandardLauncher {

	protected Logger log = LoggerFactory.getLogger(getClass());

	private OptionIniter initer = new OptionIniter();
	private Map<String, ServerContainer> serverMap = new HashMap<String, ServerContainer>();
	private Map<String, ICodecContext> codecMap = new HashMap<String, ICodecContext>();

	private DeviceIdMapping mapping;

	public StandardLauncher() {
		try {
			initer.readCfg();
		} catch (Exception e) {
			throw new RuntimeException(e);
		}
		loadServer(new PassiveTCPServer());
		loadServer(new ActiveTCPServer());
	}

	public final void start() {
		if (mapping == null) mapping = new EmptyDeviceIdMapping();
		loadFromSPI();
		for (ICodecContext codecCtx : codecMap.values()) {
			CodecOption cdecOpt = codecCtx.getOption();
			ServerContainer container = serverMap.get(cdecOpt.getServerName());
			if (container == null) {
				log.error("No server found by serverName: " + cdecOpt.getServerName() + ", for codec: " + cdecOpt.getName());
				continue;
			}
			codecCtx.getOption().checkByCommType(container.server.getCommType());
			container.codecs.add(codecCtx);
			log.info("Codec: " + codecCtx.getCodec().getName() + " bind to server: " + container.server.getName());
		}
		SubstepAwait await = SubstepAwait.create(serverMap.size(), 32 * 1000);
		for (ServerContainer container : serverMap.values()) {
			container.server.setDeviceIdMapping(mapping);
			final ServerContainer _c = container;
			await.exec(new Runnable() {
				@Override
				public void run() {
					_c.start();
					log.info("Server: " + _c.server.getName() + " started");
				}
			});
		}
		await.waitFinish();
	}

	public final void stop() {
		Utils.shareTimer.stop();
		SubstepAwait await = SubstepAwait.create(serverMap.size(), 10 * 1000);
		for (ServerContainer container : serverMap.values()) {
			final ServerContainer _c = container;
			await.exec(new Runnable() {
				@Override
				public void run() {
					_c.stop();
					log.info("Server: " + _c.server.getName() + " started");
				}
			});
		}
		await.waitFinish();
		EventManager.getInstance().stop();
	}

	protected final boolean loadServer(IServer server) {
		if (serverMap.containsKey(server.getName())) {
			log.error("Duplicate server name: " + server.getName());
			return false;
		}
		if (initer.bindServerOption(server) == null) {
			log.error("No config options found for server: " + server.getName());
			return false;
		}
		serverMap.put(server.getName(), new ServerContainer(server));
		log.info("Load server: " + server.getName() + "<" + server.getClass().getName() + "> success");
		return true;
	}

	protected final boolean loadCodec(ICodec codec) {
		if (codecMap.containsKey(codec.getName())) {
			log.error("Duplicate codec name: " + codec.getName());
			return false;
		}
		CodecOption codecOpt = initer.bindCodecOption(codec);
		if (codecOpt == null) {
			log.error("No config options found for codec: " + codec.getName());
			return false;
		}
		codecMap.put(codec.getName(), new CodecContextImpl(codec, codecOpt));
		log.info("Load codec: " + codec.getName() + "<" + codec.getClass().getName() + "> success");
		return true;
	}

	protected final boolean loadDeviceIdMapping(DeviceIdMapping mapping) {
		if (this.mapping != null) return false;
		this.mapping = mapping;
		return true;
	}

	private void loadFromSPI() {
		for (IServer server : ServiceLoader.load(IServer.class, StandardLauncher.class.getClassLoader())) {
			loadServer(server);
		}
		for (ICodec codec : ServiceLoader.load(ICodec.class, StandardLauncher.class.getClassLoader())) {
			loadCodec(codec);
		}
		for (DeviceIdMapping mapping : ServiceLoader.load(DeviceIdMapping.class, StandardLauncher.class.getClassLoader())) {
			this.mapping = mapping;
			break;
		}
	}

	private static class CodecContextImpl implements ICodecContext {

		ICodec codec;
		CodecOption option;

		CodecContextImpl(ICodec codec, CodecOption option) {
			this.codec = codec;
			this.option = option;
		}

		@Override
		public ICodec getCodec() {
			return codec;
		}

		@Override
		public CodecOption getOption() {
			return option;
		}
	}

	private static class ServerContainer {
		IServer server;
		List<ICodecContext> codecs = new LinkedList<ICodecContext>();

		ServerContainer(IServer server) {
			this.server = server;
		}

		void start() {
			server.startup(codecs);
		}

		void stop() {
			server.shutdown();
			codecs.clear();
		}
	}

	private static class EmptyDeviceIdMapping implements DeviceIdMapping {

		@Override
		public long getDeviceIdByCode(String code) {
			return -404L;
		}
	}
}
